package edu.nf.dbutil;

import edu.nf.dbutil.utils.Column;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ServiceLoader;

/**
 * @author yang
 * @date 2024-05-09
 */
public class BeanProcessor {
    /**
     * 转换器SPI
     * 在调用 BeanProcessor 类时自动创建并根据 META-INF/services/edu.nf.dbutil.TypeConverter 服务提供者接口文件,获取文件
     * 里接口全限定名初始化 ServiceLoader 对象
     */
    private static final ServiceLoader<TypeConverter> loader = ServiceLoader.load(TypeConverter.class);

    /**
     * 通过 反射 + 内省 的方式创建 bean 对象实例并对其字段进行赋值操作
     * 1）通过传入的 beanClass 参数，实例化 T 实体类对象 instance
     * 2）通过自定义的 propertyDescriptors(Class<?> beanClass) 方法获取 实体对象 的属性描述器 pds（方便对实体对象的属性赋值）
     * 3）遍历结果集对象 rs ，获取 结果集元数据信息 的列名 columnLabel
     * 4）循环解析属性描述器 pds ，调用自定义 hasColumnLabel(String columnLabel, PropertyDescriptor pd, Class<?> beanClass)
     * 方法（列名，属性描述器对象，实体类 Class 对象）比较列名和实体类字段名是否相同
     * 5）若相同，则调用自定义 processColumn(ResultSet rs, PropertyDescriptor pd, String columnLabel, Object instance)
     * 方法（结果集对象，属性描述器对象，列名，实体类）对与 列名 相同的 实体类字段名 进行赋值操作
     * 6）循环添加数据后，返回实体类 instance
     */
    public static <T> T createBean(ResultSet rs, Class<T> beanClass) throws SQLException{
        //通过 newInstance(Class<?> beanClass) 方法获取 bean 实例对象
        T instance = newInstance(beanClass);
        //获取 Bean 的属性描述器
        PropertyDescriptor[] pds = propertyDescriptors(beanClass);
        //循环遍历所有的列
        for (int i = 0; i < rs.getMetaData().getColumnCount(); i++) {
            //获取列名
            String columnLabel = rs.getMetaData().getColumnLabel( i + 1 );
            //循环解析属性描述器
            for (PropertyDescriptor pd : pds) {
                //解析匹配列字段
                if (hasColumnLabel(columnLabel, pd, beanClass)){
                    //赋值操作
                    processColumn(rs, pd, columnLabel, instance);
                }
            }
        }
        return instance;
    }

    /**
     * 解析字段获取属性描述器
     * @param beanClass 实体类 Class 对象
     * @return
     * 通过 Introspector.getBeanInfo(Class<?> beanClass, Class<?> stopClass) 方法，获取 beanClass
     * 的 BeanInfo 对象（包含有关 JavaBean 的描述性信息，例如其属性、事件、方法等），最后调用
     * beanInfo.getPropertyDescriptors() 返回属性描述符数组（包含了关于JavaBean属性的信息，如属性的名称、
     * 读取方法（getter方法）、写入方法（setter方法）、属性类型等）
     */
    private static PropertyDescriptor[] propertyDescriptors(Class<?> beanClass){
        try {
            //Introspector.getBeanInfo(Class<?> beanClass, Class<?>...stopClass)
            //这意味着 Introspector 在搜索 beanClass 及其超类以查找属性、方法等时，一旦达到 Object 类，就会停止搜索。
            BeanInfo beanInfo = Introspector.getBeanInfo(beanClass, Object.class);
            //从BeanInfo中获取属性描述器
            return beanInfo.getPropertyDescriptors();
        } catch (IntrospectionException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 根据属性描述器解析是否有匹配的列
     * @param columnLabel
     * @param pd
     * @return
     * 首先通过 pd.getName() 获取属性描述器对象 pd 的属性名称（name、age、password...） fieldName
     * 通过实体类 Class 对象（beanClass）反射方法 getDeclaredField（属性名称） 获取当前实例对象
     * 判断是否有注解，并提取注解的值（u_name、u_age、u_password）
     * 最后比较 列名 与 属性名（注解的值） 是否相同（不区分大小写）
     */
    private static boolean hasColumnLabel(String columnLabel, PropertyDescriptor pd, Class<?> beanClass){
        try {
            //属性字段名
            String fieldName = pd.getName();
            //获取Field字段实例
            Field field = beanClass.getDeclaredField(fieldName);
            //获取字段上的 @Column 注解
            if (field.isAnnotationPresent(Column.class)){
                fieldName = field.getAnnotation(Column.class).value();
            }
            //比较列名和字段名
            return fieldName.equalsIgnoreCase(columnLabel);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 创建bean实例
     * @param beanClass
     * @return
     */
    private static <T> T newInstance(Class<T> beanClass){
        try {
            return beanClass.getConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 通过rs获取value并赋值到字段上
     * @param rs
     * @param pd
     * @param columnLabel
     * 通过结果集方法 getObject(下标/列名) 获取属性值，
     *
     * （由于 mysql驱动 版本问题，数据库与 Java 的时间类型有所差异，在 mysql8.0 版本之前，Java只兼容 "java.sql" 包下的
     * "java.sql.Date"、"java.sql.Time"、"java.sql.Timestamp" 时间类型，当类中使用 "java.time.LocalDate" 等(Java8新特性)接收会
     * 报错误，在 mysql8.0 版本之后，Java 可以使用 "java.time.LocalDate"、"java.time.LocalDateTime"、"java.time.LocalTime"...
     * 接收数据库中的时间类型数据，但由于不同业务需求，数据类型需要频繁转换，所以我们需要自定义方法进行时间转换并返回）
     *
     * 遍历接口文件对象 loader 获取接口 TypeConverter 的实现类对象 converter
     * 调用方法 supports(Class<?> type) 判断 属性描述器对象 的属性全限定名称是否符合日期类型
     * 如果符合，则调用 convert(Class<?> fieldType, Object value) 方法，将 结果集数据 的日期数据转换为 实体类字段 的日期类型
     * 判断是否为基本数据类型，并且是否为空值（预防传入空的数据导致空指针异常）
     * 调用属性描述器对象的 getWriteMethod() 方法 获取 字段的 setter() 方法
     * 并且调用 invoke(bean, value) 方法进行实现赋值操作
     */
    private static void processColumn(ResultSet rs, PropertyDescriptor pd, String columnLabel, Object instance){
        try {
            //通过 rs 获取 value
            Object value = rs.getObject(columnLabel);
            //执行类型转换(例如日期不匹配)
            if (value != null) {
                for (TypeConverter converter : loader){
                    if (converter.supports(pd.getPropertyType())){
                        value = converter.convert(pd.getPropertyType(), value);
                        break;
                    }
                }
            }
            //注意：当value是null同时字段的类型是基本类型时，赋值会产生异常
            //所以需要处理这种情况， isPrimitive() 判断是否为基本数据类型
            if (pd.getPropertyType().isPrimitive() && value == null) {
                return;
            }
            //将value赋值给field，获取set方法进行赋值
            pd.getWriteMethod().invoke(instance, value);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
